
package com.trydofor.id.xnet;

import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.mina.core.service.IoService;
import org.apache.mina.core.service.IoServiceListener;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.firewall.Subnet;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ByteArrayResource;

import com.trydofor.aloe.character.Sbcs;
import com.trydofor.aloe.exception.Exceptions;
import com.trydofor.id.body.Bodies;
import com.trydofor.id.body.shell.ShellQueryBody;
import com.trydofor.id.body.shell.ShellReplyBody;
import com.trydofor.id.byta.Head;
import com.trydofor.id.byta.Packet;
import com.trydofor.id.doer.int64.Int64Doer;
import com.trydofor.id.doer.shell.Command;
import com.trydofor.id.doer.shell.Executer;
import com.trydofor.id.doer.shell.Outcome;
import com.trydofor.id.doer.shell.ShellDoer;
import com.trydofor.id.exec.Int64Cmd;
import com.trydofor.id.exec.Int64Exe;
import com.trydofor.id.exec.ServerCmd;
import com.trydofor.id.exec.ServerExe;
import com.trydofor.id.exec.SessionCmd;
import com.trydofor.id.exec.SessionExe;
import com.trydofor.id.exec.ShellCmd;
import com.trydofor.id.exec.ShellExe;
import com.trydofor.id.exec.SpringCmd;
import com.trydofor.id.exec.SpringExe;
import com.trydofor.id.exec.SystemCmd;
import com.trydofor.id.exec.SystemExe;
import com.trydofor.id.mina.LoggingFilter;
import com.trydofor.id.mina.PacketDecoder;
import com.trydofor.id.mina.PacketEncoder;
import com.trydofor.id.mina.ServerIoHandler;
import com.trydofor.id.mina.WhitelistFilter;
import com.trydofor.id.ware.SpringFactory;
import com.trydofor.id.ware.WareFactory;

/**
 * @author : Shi Rongjiu (www.trydofor.com)
 */
public final class NetServer {
    
    private static final String                      WAREHEAD = "BUILTIN/";
    private static final Logger                      LOGGER   = LoggerFactory.getLogger(NetServer.class);
    
    // 
    private final SpringFactory                      springWare;
    private final ShellDoer                          shellDoer;
    private final WhitelistFilter                    whitelist;
    private final PacketDecoder                      packetDec;
    private final LoggingFilter                      loggingFt;
    private final Int64Doer                          int64Doer;
    private final ServerIoHandler                    ioHandler;
    private final ProtocolCodecFilter                protocolc;
    private final AtomicReference<NioSocketAcceptor> serverRef;
    
    /**
     * constructor
     */
    public NetServer() {
        springWare = new SpringFactory();
        whitelist = initWhitelist();
        packetDec = initDecoder();
        loggingFt = new LoggingFilter(LOGGER);
        shellDoer = new ShellDoer();
        int64Doer = new Int64Doer();
        ioHandler = new ServerIoHandler();
        ioHandler.register(shellDoer);
        ioHandler.register(int64Doer);
        protocolc = new ProtocolCodecFilter(new PacketEncoder(), packetDec);
        serverRef = new AtomicReference<NioSocketAcceptor>();
        
        initBuiltinWare();
        initBuiltinCommand();
    }
    
    /**
     * start the server at the host:port
     * 
     * @param isa the host and port
     * @throws Exception if got exception
     */
    public synchronized void start(InetSocketAddress isa) throws Exception {
        NioSocketAcceptor server = serverRef.get();
        if (server == null || !server.isActive()) {
            server = new NioSocketAcceptor();
            server.getFilterChain().addLast("whitelist", whitelist);
            server.getFilterChain().addLast("codecer", protocolc);
            server.getFilterChain().addLast("logging", loggingFt);
            server.setHandler(ioHandler);
            server.setCloseOnDeactivation(false); // true got zombie in disposing
            server.setReuseAddress(true);
            serverRef.set(server);
            LOGGER.info("init server");
        }
        server.bind(isa);
        LOGGER.info("Server binding to " + isa);
    }
    
    /**
     * stop the host:port, stop the server if no port left
     * 
     * @param isa the host and port, null means all
     */
    public void stop(InetSocketAddress isa) {
        NioSocketAcceptor server = serverRef.get();
        if (server == null) {
            LOGGER.info("Server is null");
            return;
        }
        if (isa == null) {
            for (IoSession s : server.getManagedSessions().values()) {
                LOGGER.info("closing session: " + s);
                s.close(true);
            }
            server.unbind(); // zombie when CloseOnDeactivation=true
            LOGGER.info("Server undind all");
        }
        else {
            for (IoSession s : server.getManagedSessions().values()) {
                if (s.getLocalAddress().equals(isa)) {
                    LOGGER.info("closing session: " + s);
                    s.close(true);
                }
            }
            server.unbind(isa);
            LOGGER.info("Server undind:" + isa);
        }
        
        // dispose if out of service
        if (server.getLocalAddresses().isEmpty()) {
            LOGGER.info("Server is disposing");
            serverRef.set(null);
            server.dispose(false); //true got zombie in disposing
        }
    }
    
    /**
     * whether the server is running
     * 
     * @return true if running
     */
    public boolean isActive() {
        NioSocketAcceptor server = serverRef.get();
        if (server == null) return false;
        return server.isActive();
    }
    
    /**
     * get the ware factory
     * 
     * @return the context
     */
    public WareFactory getWareFactory() {
        return springWare;
    }
    
    /**
     * execute a command
     * 
     * @param cmd the command
     * @return the outcome
     */
    public Outcome execCommand(ShellQueryBody cmd) {
        if (cmd == null) return null;
        Packet p = new Packet(new Head(0, 0), cmd);
        Packet r = shellDoer.response(p);
        
        return ((ShellReplyBody) r.getBody()).getOutcome();
    }
    
    // ---------------------------
    private void initBuiltinWare() {
        springWare.registerSingleton(WAREHEAD + "SPRINGWARE", springWare);
        springWare.registerSingleton(WAREHEAD + "WHITELIST", whitelist);
        springWare.registerSingleton(WAREHEAD + "PACKETDEC", packetDec);
        springWare.registerSingleton(WAREHEAD + "LOGGINGFT", loggingFt);
        springWare.registerSingleton(WAREHEAD + "SHELLDOER", shellDoer);
        springWare.registerSingleton(WAREHEAD + "INT64DOER", int64Doer);
        springWare.registerSingleton(WAREHEAD + "SERVERREF", serverRef);
    }
    
    private void initBuiltinCommand() {
        // shell
        Command c01 = ShellCmd.registerCommand();
        Command c02 = ShellCmd.unregisterCommand();
        ShellExe e0 = new ShellExe(shellDoer, springWare);
        shellDoer.register("shell/" + c01.getAlias(), c01, e0);
        shellDoer.register("shell/" + c02.getAlias(), c02, e0);
        
        // server
        Command c11 = ServerCmd.listSessions();
        Command c12 = ServerCmd.serverStatus();
        Command c13 = ServerCmd.stopServer();
        Command c14 = ServerCmd.serverBounds();
        Command c15 = ServerCmd.cmdsInXml();
        ServerExe e1 = new ServerExe(serverRef);
        shellDoer.register("server/" + c11.getAlias(), c11, e1);
        shellDoer.register("server/" + c12.getAlias(), c12, e1);
        shellDoer.register("server/" + c13.getAlias(), c13, new Executer() {
            
            @Override
            public Outcome execute(Command command, Map<String, String> argdata) {
                stop(null);
                return new Outcome(0, "server stopping ...", null);
            }
        });
        shellDoer.register("server/" + c14.getAlias(), c14, e1);
        shellDoer.register("server/" + c15.getAlias(), c15, new Executer() {
            
            @Override
            public Outcome execute(Command command, Map<String, String> argdata) {
                InputStream cn = ConfBuilder.loadStream(argdata.get("file"));
                if (cn == null) {
                    return new Outcome(-1, null, "file not found:" + argdata.get("file"));
                }
                StringBuilder sb = new StringBuilder();
                try {
                    List<ShellQueryBody> cmds = ConfBuilder.buildCmd(cn);
                    sb.append("the count of commands: ").append(cmds.size());
                    int i = 1;
                    for (ShellQueryBody cmd : cmds) {
                        sb.append("\n=== ").append(i).append(" ===\n");
                        sb.append(cmd.getCommand()).append("\n");
                        Outcome out = execCommand(cmd);
                        sb.append("\n").append(out);
                        i++;
                    }
                    return new Outcome(0, sb.toString(), null);
                }
                catch (Exception e) {
                    LOGGER.warn("failed to exec commands", e);
                    return new Outcome(-2, null, Exceptions.printStackTrace(e));
                }
            }
        });
        
        // session
        Command c21 = SessionCmd.sessionConfig();
        Command c22 = SessionCmd.closeSession(serverRef);
        Command c23 = SessionCmd.sessionStatus(serverRef);
        SessionExe e2 = new SessionExe(serverRef);
        shellDoer.register("session/" + c21.getAlias(), c21, e2);
        shellDoer.register("session/" + c22.getAlias(), c22, e2);
        shellDoer.register("session/" + c23.getAlias(), c23, e2);
        
        // int64
        Command c31 = Int64Cmd.registerMaker();
        Command c32 = Int64Cmd.unregisterMaker();
        Command c33 = Int64Cmd.query();
        Command c34 = Int64Cmd.listMaker();
        Int64Exe e3 = new Int64Exe(int64Doer, springWare);
        shellDoer.register("int64/" + c31.getAlias(), c31, e3);
        shellDoer.register("int64/" + c32.getAlias(), c32, e3);
        shellDoer.register("int64/" + c33.getAlias(), c33, e3);
        shellDoer.register("int64/" + c34.getAlias(), c34, e3);
        
        // spring
        Command c41 = SpringCmd.loadConfig();
        Command c42 = SpringCmd.listBeans();
        Command c43 = SpringCmd.removeBean();
        Command c44 = SpringCmd.getBean();
        SpringExe e4 = new SpringExe(springWare);
        shellDoer.register("spring/" + c41.getAlias(), c41, e4);
        shellDoer.register("spring/" + c42.getAlias(), c42, e4);
        shellDoer.register("spring/" + c43.getAlias(), c43, e4);
        shellDoer.register("spring/" + c44.getAlias(), c44, e4);
        
        // system
        Command c51 = SystemCmd.addClassPath();
        Command c52 = SystemCmd.classForName();
        SystemExe e5 = new SystemExe();
        shellDoer.register("system/" + c51.getAlias(), c51, e5);
        shellDoer.register("system/" + c52.getAlias(), c52, e5);
    }
    
    private WhitelistFilter initWhitelist() {
        WhitelistFilter whitelist = new WhitelistFilter();
        try {
            whitelist.allow(new Subnet(InetAddress.getByName("127.0.0.0"), 8));
            for (InetAddress adr : ConfBuilder.getInetAddress()) {
                whitelist.allow(adr);
            }
        }
        catch (UnknownHostException e) {
            // ignore
        }
        return whitelist;
    }
    
    private PacketDecoder initDecoder() {
        PacketDecoder dc = new PacketDecoder();
        dc.register(Bodies.INT64QUERY);
        dc.register(Bodies.INT64REPLY);
        dc.register(Bodies.SHELLQUERY);
        dc.register(Bodies.SHELLREPLY);
        dc.register(Bodies.USAGEQUERY);
        dc.register(Bodies.USAGEREPLY);
        return dc;
    }
    
    /**
     * Entrance
     * 
     * @param args args
     * @throws Exception if got exceptioin
     */
    public static void main(String[] args) throws Exception {
        // init config
        Config conf = null;
        if (args.length >= 1) { // assigned config
            InputStream cn = ConfBuilder.loadStream(args[0]);
            if (cn == null) {
                LOGGER.error("can not find config: " + args[0]);
                System.exit(-1); // exit
            }
            conf = ConfBuilder.buildCnf(cn);
            LOGGER.info("use config: " + args[0]);
        }
        else { // default
            InputStream cn = ConfBuilder.loadStream(Constant.SERVICE_CONF);
            if (cn == null) {
                LOGGER.warn("can not find config:" + Constant.SERVICE_CONF + ", use default setting.");
                conf = ConfBuilder.buildDefault();
            }
            else {
                conf = ConfBuilder.buildCnf(cn);
                LOGGER.info("use config: " + Constant.SERVICE_CONF);
            }
        }
        
        NetServer ns = new NetServer();
        
        // init ware(spring)
        String spr = Sbcs.isNullOrEmpty(conf.getWare()) ? Constant.SPRING_CONF : conf.getWare();
        InputStream sp = ConfBuilder.loadStream(spr);
        if (sp == null) {
            LOGGER.warn("can not find spring config: " + spr);
        }
        else {
            int cnt = ns.springWare.registerXmlConfBeans(new ByteArrayResource(ConfBuilder.streamToBytes(sp)));
            LOGGER.info("init beans from config. count=" + cnt);
        }
        
        // bind server
        for (InetSocketAddress isa : conf.getBindHosts()) {
            ns.start(isa);
        }
        
        // init command
        for (ShellQueryBody cmd : conf.getCommands()) {
            LOGGER.info("execute command: " + cmd.getCommand());
            Outcome out = ns.execCommand(cmd);
            LOGGER.info(String.valueOf(out));
        }
        
        // wait
        final Thread waitingThread = Thread.currentThread();
        ns.serverRef.get().addListener(new IoServiceListener() {
            
            @Override
            public void sessionDestroyed(IoSession session) throws Exception {
            }
            
            @Override
            public void sessionCreated(IoSession session) throws Exception {
            }
            
            @Override
            public void serviceIdle(IoService service, IdleStatus idleStatus) throws Exception {
            }
            
            @Override
            public void serviceDeactivated(IoService service) throws Exception {
                waitingThread.interrupt();
            }
            
            @Override
            public void serviceActivated(IoService service) throws Exception {
            }
        });
        
        // wait
        try {
            waitingThread.join();
        }
        catch (InterruptedException e) {
            // ignore
        }
    }
}
